package yicr.geoHash;


import java.util.*;

public class             GeoHash {
public static final double MINLAT = -90;
public static final double MAXLAT = 90;
public static final double MINLNG = -180;
public static final double MAXLNG = 180;

// 默认20bit 对应8个字符的geohash编码，精度为+/-0.019km
private  int numbits = 4 * 5; //经纬度单独编码长度

private  double minLatInterval =0;
private  double minLngInterval =0;

private final static char[] digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8',
        '9', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p',
        'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' };

//定义编码映射关系
final static HashMap<Character, Integer> lookup = new HashMap<Character, Integer>();
//初始化编码映射内容
static {
    int i = 0;
    for (char c : digits)
        lookup.put(c, i++);
}

public GeoHash(){
    setMinLatLngInterval();
}

public GeoHash(int numbits){
    this.numbits=numbits;
    setMinLatLngInterval();
}

public String encode(double lat, double lon) {
    BitSet latbits = getBits(lat, -90, 90);
    BitSet lonbits = getBits(lon, -180, 180);
    StringBuilder buffer = new StringBuilder();
    for (int i = 0; i < numbits; i++) {
        buffer.append( (lonbits.get(i))?'1':'0');
        buffer.append( (latbits.get(i))?'1':'0');
    }
    String code = base32(Long.parseLong(buffer.toString(), 2));
    //Log.i("okunu", "encode  lat = " + lat + "  lng = " + lon + "  code = " + code);
    return code;
}


    //encode
    public String encode(long longLongitude,long longLatitude){
        StringBuilder stringBuilder=new StringBuilder();
        for(int i=0;i<numbits;i++){
            long baseLong=1L<<i;
            if((longLongitude & baseLong) == 0){
                stringBuilder.append('0');
            }else {
                stringBuilder.append('1');
            }

            if((longLatitude & baseLong) == 0){
                stringBuilder.append('0');
            }else {
                stringBuilder.append('1');
            }
        }

        String code = base32(Long.parseLong(stringBuilder.toString(), 2));
        return code;
}


public ArrayList<String> getArroundGeoHash(double lat, double lon){
    //Log.i("okunu", "getArroundGeoHash  lat = " + lat + "  lng = " + lon);
    ArrayList<String> list = new ArrayList<>();
    double uplat = lat + minLatInterval;
    double downLat = lat - minLatInterval;

    double leftlng = lon - minLngInterval;
    double rightLng = lon + minLngInterval;

    String leftUp = encode(uplat, leftlng);
    list.add(leftUp);

    String leftMid = encode(lat, leftlng);
    list.add(leftMid);

    String leftDown = encode(downLat, leftlng);
    list.add(leftDown);

    String midUp = encode(uplat, lon);
    list.add(midUp);

    String midMid = encode(lat, lon);
    list.add(midMid);

    String midDown = encode(downLat, lon);
    list.add(midDown);

    String rightUp = encode(uplat, rightLng);
    list.add(rightUp);

    String rightMid = encode(lat, rightLng);
    list.add(rightMid);

    String rightDown = encode(downLat, rightLng);
    list.add(rightDown);

    //Log.i("okunu", "getArroundGeoHash list = " + list.toString());
    return list;
}

//根据经纬度和范围，获取对应的二进制
public BitSet getBits(double lat, double floor, double ceiling) {
    BitSet buffer = new BitSet(numbits);
    for (int i = 0; i < numbits; i++) {
        double mid = (floor + ceiling) / 2;
        if (lat >= mid) {
            buffer.set(i);
            floor = mid;
        } else {
            ceiling = mid;
        }
    }
    return buffer;
}

//将经纬度合并后的二进制进行指定的32位编码
private String base32(long i) {
    char[] buf = new char[65];
    int charPos = 64;
    boolean negative = (i < 0);
    if (!negative){
        i = -i;
    }
    while (i <= -32) {
        buf[charPos--] = digits[(int) (-(i % 32))];
        i /= 32;
    }
    buf[charPos] = digits[(int) (-i)];
    if (negative){
        buf[--charPos] = '-';
    }
    return new String(buf, charPos, (65 - charPos));
}

private void setMinLatLngInterval() {
    minLatInterval = MAXLAT - MINLAT;
    for (int i = 0; i < numbits; i++) {
        minLatInterval /= 2.0;
    }
    minLngInterval = MAXLNG - MINLNG;
    for (int i = 0; i < numbits; i++) {
        minLngInterval /= 2.0;
    }
}

public double getMinLatInterval(){
    if(minLatInterval==0){
        setMinLatLngInterval();
    }
    return minLatInterval;
}

public double getMinLngInterval(){
    if(minLngInterval==0){
        setMinLatLngInterval();
    }
    return minLngInterval;
}

//根据二进制和范围解码
private double decode(BitSet bs, double floor, double ceiling) {
    double mid = 0;
    for (int i=0; i<bs.length(); i++) {
        mid = (floor + ceiling) / 2;
        if (bs.get(i))
            floor = mid;
        else
            ceiling = mid;
    }
    return mid;
}



    //对编码后的字符串解码

    /**
     *
     * @param geohash
     * @return [lat,long]
     */
    public double[] decode(String geohash) {
        StringBuilder buffer = new StringBuilder();
        for (char c : geohash.toCharArray()) {
            int i = lookup.get(c) + 32;
            buffer.append( Integer.toString(i, 2).substring(1) );
        }

        BitSet lonset = new BitSet();
        BitSet latset = new BitSet();

        //偶数位，经度
        int j =0;
        for (int i=0; i< numbits*2;i+=2) {
            boolean isSet = false;
            if ( i < buffer.length() )
                isSet = buffer.charAt(i) == '1';
            lonset.set(j++, isSet);
        }

        //奇数位，纬度
        j=0;
        for (int i=1; i< numbits*2;i+=2) {
            boolean isSet = false;
            if ( i < buffer.length() )
                isSet = buffer.charAt(i) == '1';
            latset.set(j++, isSet);
        }

        double lon = decode(lonset, -180, 180);
        double lat = decode(latset, -90, 90);

        return new double[] {lat, lon};
    }



    public PointGis decode2PointGis(String geoHash){
        double[] latlon=decode(geoHash);
        return new PointGis(geoHash,latlon[1],latlon[0]);
    }


    public double[] getCeilingAndFloor(double longitude,double floor, double ceiling ){
        double[] ceilingAndFloor=new double[2];
        double mid=0;
        BitSet buffer = new BitSet(numbits);
        for (int i = 0; i < numbits; i++) {
            if(longitude>=mid){
                buffer.set(i);
                floor=mid;
            }else{
                ceiling=mid;
            }
            mid=(ceiling+floor)/2;
        }

        ceilingAndFloor[0]=ceiling;
        ceilingAndFloor[1]=floor;
        return ceilingAndFloor;
    }

    public double getLongitudeInterval(){
        double[] ceilingAndFloor=getCeilingAndFloor(0,-180,180);
        return ceilingAndFloor[0]-ceilingAndFloor[1];
    }

    public double getLatitudeInterval(){
        double[] ceilingAndFloor=getCeilingAndFloor(0,-90,90);
        return ceilingAndFloor[0]-ceilingAndFloor[1];
    }

    public double getMid(double longitude,double floor, double ceiling){
        double[] ceilingAndFloor=getCeilingAndFloor(longitude,floor,ceiling);
        return (ceilingAndFloor[0]+ceilingAndFloor[1])/2;
    }

    public PointGis getMid(PointGis pointGis){
        double[] longitudeCeilingAndFloor=getCeilingAndFloor(pointGis.getLongitude(),-180,180);
        double[] latitudeCeilingAndFloor=getCeilingAndFloor(pointGis.getLatitude(),-90,90);
        return new PointGis(null,(longitudeCeilingAndFloor[0]+longitudeCeilingAndFloor[1])/2,(latitudeCeilingAndFloor[0]+latitudeCeilingAndFloor[1])/2);
    }

    /**
     * 返回制定经纬点所在 geohash 所在矩形的4个定点,顺序为wise clock
     */
    List<PointGis> getVertexListByLonlat(double longitude,double latitude){
        double[] ceilingAndFloorOfLon=getCeilingAndFloor(longitude,-180,180);
        double[] ceilingAndFloorOfLat=getCeilingAndFloor(latitude,-90,90);
        PointGis pointGis1=new PointGis("0",ceilingAndFloorOfLon[1],ceilingAndFloorOfLat[0]);
        PointGis pointGis2=new PointGis("1",ceilingAndFloorOfLon[0],ceilingAndFloorOfLat[0]);
        PointGis pointGis3=new PointGis("2",ceilingAndFloorOfLon[0],ceilingAndFloorOfLat[1]);
        PointGis pointGis4=new PointGis("3",ceilingAndFloorOfLon[1],ceilingAndFloorOfLat[1]);
        List<PointGis> list=new ArrayList<>(4);
        list.add(pointGis1);
        list.add(pointGis2);
        list.add(pointGis3);
        list.add(pointGis4);
        return list;
    }

public static void main(String[] args)  throws Exception{

    //114.28223	35.76942 ww372x9g
    //35.8539481775  114.1664038791    ww35yp1b
    /*GeoHash geohash = new GeoHash();
    double longitude=110.074078;
    double latitude=31.220010;

    String s = geohash.encode(latitude, longitude);
    System.out.println(s);

    String geoHashCode="ww372x9g";

    double[] boundary=geohash.decode(geoHashCode);
    System.out.println(boundary[0]);
    System.out.println(boundary[1]);

    geoHashCode="ww372x9h";
    boundary=geohash.decode(geoHashCode);
    System.out.println(boundary[0]);
    System.out.println(boundary[1]);


    double[] latitudes=geohash.getCeilingAndFloor(latitude,-90,90);
    double[] longitudes=geohash.getCeilingAndFloor(longitude,-180,180);
    System.out.println(latitudes[0] + " ; " + latitudes[1]);
    System.out.println(longitudes[0] + "  ;   " + longitudes[1]);


    geohash.getArroundGeoHash(40.222012, 116.248283);
//        double[] geo = geohash.decode(s);
//        System.out.println(geo[0]+" "+geo[1]);

    BitSet bitSet=new BitSet();
    bitSet.set(0);
    bitSet.set(1);
    long longValue=geohash.getLongFromBitMap(bitSet);

    System.out.println(longValue);


    BitSet longitudeBitSet=geohash.getBits(longitude,-180,180);
    BitSet latitudeBistSet=geohash.getBits(latitude,-90,90);

    Long longLongitude=geohash.getLongFromBitMap(longitudeBitSet);
    Long longLatitude=geohash.getLongFromBitMap(latitudeBistSet);

    String geoHashString1=geohash.encode(longLongitude,longLatitude);

    System.out.println(geoHashString1);


    BitSet bitSet1=geohash.long2BitSet(15L);
    System.out.println(bitSet1);

    //region geoHashVaule
    //110.074078    31.220010   117.288830  36.606096  out of memory
    double regionLongitude1=113.547580 ;
    double regionLatitude1=35.504755;
    double regionLongitude2=115.546984;//110.224668
    double regionLatitude2=36.393111;

    System.out.println(geohash.encode(regionLatitude1,regionLongitude1));


    System.out.println("******************************************************");
    String geoHashValue1="wmw38g3m";
    System.out.println(geohash.decode(geoHashValue1)[0]);
    System.out.println(geohash.decode(geoHashValue1)[1]);

    double longitude1=geohash.decode(geoHashValue1)[1];
    double latitude1=geohash.decode(geoHashValue1)[0];

    System.out.println("******************** bitSet 1 output **********************");
    System.out.println(geohash.getBits(longitude1, -180, 180));
    System.out.println(geohash.getBits(latitude1, -90, 90));

    long longLongitude1=geohash.getLongFromBitMap(geohash.getBits(longitude1,-180,180));
    long longLatitude1=geohash.getLongFromBitMap(geohash.getBits(latitude1,-90,90));

    System.out.println("******************** bitSet 2 output **********************");
    System.out.println(geohash.long2BitSet(longLongitude1));
    System.out.println(geohash.long2BitSet(longLatitude1));


    System.out.println(geohash.encode(longLongitude1, longLatitude1));

    BitSet bitSetLongitude= geohash.long2BitSet(longLongitude1);
    BitSet bitSetLatitude=geohash.long2BitSet(longLatitude1);

    System.out.println(geohash.encode(longLongitude1, longLatitude1));


    Map<String,PointGis> map=geohash.calGeohashMap4Region(regionLongitude1,regionLatitude1,regionLongitude2,regionLatitude2);

    Set<String> keySet=map.keySet();
    for (String s1 : keySet) {
        System.out.println(s1 + " :  " + map.get(s1).getLongitude() + "  ;   " + map.get(s1).getLatitude());
    }

    System.out.println("count = " + keySet.size())*/;
}


    //bitSet max(index)<64
    public long getLongFromBitMap(BitSet bitSet){
        long value=0;
        long[] longArray=bitSet.toLongArray();
        value=longArray[0];
        return value;
    }


    public BitSet long2BitSet(long l){
        BitSet bitSet=new BitSet();
        for (int i = 0; i < 64; i++) {
            if((l & (1L<<i)) != 0){
                bitSet.set(i);
            }
        }

        return  bitSet;
    }

    // 给一个经纬度区域范围,返回区域内所有的<geohashvalue,points>,其中points 为中心点经纬度
    Map<String, PointGis> calGeohashMap4Region(double longitude1,double latitude1,double longitude2,double latitude2 ){
        Map<String, PointGis> map=new HashMap<>();

        double maxLongitude=Math.max(longitude1,longitude2);
        double minLongitude=Math.min(longitude1,longitude2);


        double maxLatitude=Math.max(latitude1,latitude2);
        double minLatitude=Math.min(latitude1,latitude2);


        double currentLongitude=minLongitude;
        double currentLatitude=minLatitude;
        double midLongitude=0;
        double midLatitude=0;

        String geoHashValue=null;

        //System.out.println(minLongitude + "-" + maxLongitude);
        //System.out.println(minLatitude + "-" + maxLatitude);


        //System.out.println(minLat);
        //System.out.println(minLng);

        int i=0;

        while (currentLatitude<=maxLatitude){
            while (currentLongitude<=maxLongitude){
               /* if(i==10){
                    break;
                }*/

                geoHashValue=encode(currentLatitude,currentLongitude);
                midLongitude=getMid(currentLongitude,-180,180);
                midLatitude=getMid(currentLatitude,-90,90);

                PointGis pointGis=new PointGis(geoHashValue,midLongitude,midLatitude);

                //System.out.println(pointGis);
                map.put(geoHashValue,pointGis);


                currentLongitude+= minLngInterval;

                i++;
                System.out.println(i);
            }
            //break;
            currentLatitude+= minLatInterval;
            currentLongitude=minLongitude;
        }

        return map;
    }



}